using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using PS.WebParts.Rollup.Query;
using PS.WebParts.Rollup.DAL.OM;

namespace PS.WebParts.Rollup.Query
{
    public class ParsingException : Exception
    {
        public ParsingException(string message)
            : base(message)
        {
        }
    }

    public class CAMLParser
    {
        private class OrderByItem
        {
            private string fieldName;
            private bool ascending = true;

            public string FieldName
            {
                get { return fieldName; }
                set { fieldName = value; }
            }

            public bool Ascending
            {
                get { return ascending; }
                set { ascending = value; }
            }
        }

        internal enum CompareOperation
        {
            Equal,
            GreaterOrEqual,
            Greater,
            LessOrEqual,
            Less,
            NotEqual
        }

        internal enum ArgumentType
        {
            Constant,
            Field,
            UserId,
            DateTime
        }

        internal class CompareOp
        {
            private CompareOperator opCode;
            private string firstArg;
            private string secondArg;
            private ArgumentType secondArgType;

            public string FirstArg
            {
                get { return firstArg; }
                set { firstArg = value; }
            }

            public string SecondArg
            {
                get { return secondArg; }
                set { secondArg = value; }
            }

            public ArgumentType SecondArgType
            {
                get { return secondArgType; }
                set { secondArgType = value; }
            }

            public CompareOp(string camlOperator)
            {
                InitFromString(camlOperator);
            }

            private void InitFromString(string camlOperator)
            {
                switch (camlOperator)
                {
                    case "Eq":
                        opCode = CompareOperator.Equal;
                        break;
                    case "Geq":
                        opCode = CompareOperator.GreaterOrEqual;
                        break;
                    case "Gt":
                        opCode = CompareOperator.Greater;
                        break;
                    case "Leq":
                        opCode = CompareOperator.LessOrEqual;
                        break;
                    case "Lt":
                        opCode = CompareOperator.Less;
                        break;
                    case "Neq":
                        opCode = CompareOperator.NotEqual;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException("camlOperator", "Undefined CAML operator: " + camlOperator);
                }
            }

            public WhereTerm Render(out WhereClause whereClause)
            {
                SqlExpression secondArgExpression;
                whereClause = null;

                switch (secondArgType)
                {
                    case ArgumentType.Field:
                        secondArgExpression = SqlExpression.Field(secondArg);
                        break;

                    case ArgumentType.Constant:
                        secondArgExpression = SqlExpression.Raw('\'' + secondArg + '\'');
                        break;

                    case ArgumentType.DateTime:
                        secondArgExpression = SqlExpression.Raw(secondArg);
                        break;

                    case ArgumentType.UserId:
                        {
                            whereClause = new WhereClause(WhereClauseRelationship.Or);
                            secondArgExpression = SqlExpression.Raw('\'' + secondArg + '\'');
                            if (!QueryParams.Instance.UserFields.ContainsValue(firstArg))
                            {
                                whereClause.Terms.Add(
                                    WhereTerm.CreateCompare(SqlExpression.Raw(firstArg), secondArgExpression, opCode));
                            }
                            else
                            {
                                whereClause.Terms.Add(
                                    WhereTerm.CreateCompare(SqlExpression.Raw(firstArg),
                                                            SqlExpression.Raw('\'' + QueryParams.Instance.UserName + '\''),
                                                            opCode));
                            }
                            break;
                        }

                    default:
                        throw new ArgumentOutOfRangeException("undefined argument type");
                }

                return WhereTerm.CreateCompare(SqlExpression.Raw(firstArg), secondArgExpression, opCode);
            }
            //protected abstract CompareOperator GetOP();
        }


        //internal class EqOp : CompareOp
        //{
        //    protected override CompareOperator GetOP()
        //    {
        //        return CompareOperator.Equal;
        //    }
        //}

        //internal class GeqOp : CompareOp
        //{
        //    protected override CompareOperator GetOP()
        //    {
        //        return CompareOperator.GreaterOrEqual;
        //    }
        //}

        //internal class GtOp : CompareOp
        //{
        //    protected override CompareOperator GetOP()
        //    {
        //        return CompareOperator.Greater;
        //    }
        //}

        internal abstract class BitwiseOp
        {
            private List<BitwiseOp> childBitwiseOps = new List<BitwiseOp>();
            private List<CompareOp> compareOps = new List<CompareOp>();
            private List<WhereTerm> whereTerms = new List<WhereTerm>();

            public List<BitwiseOp> ChildBitwiseOps
            {
                get { return childBitwiseOps; }
                set { childBitwiseOps = value; }
            }

            public List<CompareOp> CompareOps
            {
                get { return compareOps; }
                set { compareOps = value; }
            }

            public List<WhereTerm> WhereTerms
            {
                get { return whereTerms; }
                set { whereTerms = value; }
            }

            //public abstract string Render();
            public abstract WhereClause Render();

            protected void RenderImpl(WhereClause res)
            {
                if (childBitwiseOps != null && childBitwiseOps.Count > 0)
                {
                    foreach (BitwiseOp childBitwiseOp in childBitwiseOps)
                    {
                        res.SubClauses.Add(childBitwiseOp.Render());
                    }
                }

                foreach (CompareOp compareOp in compareOps)
                {
                    WhereClause wc;
                    WhereTerm wt = compareOp.Render(out wc);
                    if (wc != null)
                    {
                        res.SubClauses.Add(wc);
                    }
                    else
                    {
                        res.Terms.Add(wt);
                    }
                }

                res.Terms.AddRange(WhereTerms.ToArray());
            }
        }

        internal class AndOp : BitwiseOp
        {
            //override public string Render()
            //{
            //    string res = string.Empty;
            //    //if(childBitwiseOp != null)
            //    //{
            //    //    res = '(' + childBitwiseOp.Render() + ')';
            //    //}
            //    //else
            //    //{
            //    //    if(compareOps.Count > 0)
            //    //    {
            //    //        StringBuilder sb = new StringBuilder();
            //    //        sb.Append('(');
            //    //        for (int i = 0; i < compareOps.Count - 1; i++)
            //    //        {
            //    //            CompareOp compareOp = compareOps[i];
            //    //            sb.Append(compareOp.Render());
            //    //            sb.Append(", AND ");
            //    //        }
            //    //        sb.Append(compareOps[compareOps.Count - 1].Render());
            //    //        sb.Append(')');

            //    //        res = sb.ToString();
            //    //    }
            //    //}

            //    return res;
            //}

            override public WhereClause Render()
            {
                WhereClause res = new WhereClause(WhereClauseRelationship.And);
                RenderImpl(res);
                return res;
            }
        }

        internal class OrOp : BitwiseOp
        {
            public override WhereClause Render()
            {
                WhereClause res = new WhereClause(WhereClauseRelationship.Or);
                RenderImpl(res);
                return res;
            }
        }

        private List<OrderByItem> orderBys = new List<OrderByItem>();
        private List<BitwiseOp> bitwiseOps = new List<BitwiseOp>();
        private List<CompareOp> compareOps = new List<CompareOp>();
        private List<WhereTerm> whereTerms = new List<WhereTerm>();

        private Dictionary<string, string> accessibleFields;
        private Dictionary<string, string> allFields;
        private int userID;
        //private Dictionary<string, LookupField> lookupFields;
        //private Dictionary<string, string> userFields;

        private readonly string onlyDateFunction = "(dateadd(dd,0, datediff(dd,0, {0})))";

        public CAMLParser(Dictionary<string, string> accessibleFields,
                          Dictionary<string, string> allFields,
                          Dictionary<string, LookupField> lookupFields,
                          Dictionary<string, string> userFields,
                          int userID)
        {
            this.accessibleFields = CloneDictionary(accessibleFields);
            this.allFields = CloneDictionary(allFields);
            this.userID = userID;

            foreach (KeyValuePair<string, string> field in userFields)
            {
                this.allFields.Add(field.Key, field.Value);
                this.accessibleFields.Add(field.Key, field.Value);
            }

            foreach (KeyValuePair<string, LookupField> lookupField in lookupFields)
            {
                this.allFields.Add(lookupField.Key, lookupField.Value.ColumnnName);
                this.accessibleFields.Add(lookupField.Key, lookupField.Value.ColumnnName);
            }

            //this.lookupFields = lookupFields;
            //this.userFields = userFields;
        }

        private static Dictionary<T1, T2> CloneDictionary<T1, T2>(Dictionary<T1, T2> dic)
        {
            Dictionary<T1, T2> res = new Dictionary<T1, T2>(dic.Count);
            foreach (KeyValuePair<T1, T2> pair in dic)
            {
                res.Add(pair.Key, pair.Value);
            }
            return res;
        }

        //private CompareOperation FromCamlOp(string camlOperator)
        //{
        //    switch(camlOperator)
        //    {
        //        case "Eq":
        //            return CompareOperation.Equal;
        //        case "Geq":
        //            return CompareOperation.GreaterOrEqual;
        //        case "Gt":
        //            return CompareOperation.Greater;
        //        case "Leq":
        //            return CompareOperation.LessOrEqual;
        //        case "Lt":
        //            return CompareOperation.Less;
        //        case "Neq":
        //            return CompareOperation.NotEqual;
        //        default:
        //            throw new ArgumentOutOfRangeException("camlOperator", "Undefined CAML operator: " + camlOperator);
        //    }
        //}

        public void Parse(XmlDocument query)
        {
            if (query.DocumentElement.LocalName == "Query")
            {
                ParseQuery(query.DocumentElement);
            }
        }

        public void Parse(string query)
        {
            if (!query.ToLowerInvariant().StartsWith("<Query>".ToLowerInvariant()))
            {
                query = "<Query>" + query + "</Query>";
            }
            XmlDocument xDoc = new XmlDocument();
            xDoc.LoadXml(query);
            Parse(xDoc);
        }

        private void ParseQuery(XmlElement query)
        {
            foreach (XmlNode childNode in query.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    switch (childNode.LocalName)
                    {
                        case "OrderBy":
                            ParseOrderBy(childNode);
                            break;
                        case "Where":
                            ParseWhere(childNode);
                            break;
                        default:
                            break;
                    }
                }
            }
        }

        private void ParseWhere(XmlNode whereNode)
        {
            foreach (XmlNode childNode in whereNode.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    switch (childNode.LocalName)
                    {
                        case "And":
                            {
                                ParseAnd(childNode, null);
                                break;
                            }
                        case "Or":
                            {
                                ParseOr(childNode, null);
                                break;
                            }
                        case "BeginsWith":
                            {
                                ParseBeginsWith(childNode, null);
                                break;
                            }
                        case "Contains":
                            {
                                ParseContains(childNode, null);
                                break;
                            }
                        case "Eq":
                        case "Geq":
                        case "Gt":
                        case "Leq":
                        case "Lt":
                        case "Neq":
                            {
                                ParseCompareOperator(childNode, null);
                                break;
                            }
                        case "IsNotNull":
                            {
                                ParseIsNotNull(childNode, null);
                                break;
                            }
                        case "IsNull":
                            {
                                ParseIsNull(childNode, null);
                                break;
                            }
                        default:
                            throw new ArgumentOutOfRangeException("node name",
                                "Node " + childNode.LocalName + " is not supported");
                    }
                }
            }
        }

        private void ParseAnd(XmlNode andNode, BitwiseOp root)
        {
            AndOp andOp = new AndOp();
            ParseBitwiseOp(andNode, andOp, root);
        }

        private void ParseBitwiseOp(XmlNode andNode, BitwiseOp bitwiseOp, BitwiseOp root)
        {
            if (root == null)
            {
                bitwiseOps.Add(bitwiseOp);
            }
            else
            {
                root.ChildBitwiseOps.Add(bitwiseOp);
            }

            foreach (XmlNode childNode in andNode.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    switch (childNode.LocalName)
                    {
                        case "And":
                            {
                                ParseAnd(childNode, bitwiseOp);
                                break;
                            }
                        case "Or":
                            {
                                ParseOr(childNode, bitwiseOp);
                                break;
                            }
                        case "BeginsWith":
                            {
                                ParseBeginsWith(childNode, bitwiseOp);
                                break;
                            }
                        case "Contains":
                            {
                                ParseContains(childNode, bitwiseOp);
                                break;
                            }
                        case "Eq":
                        case "Geq":
                        case "Gt":
                        case "Leq":
                        case "Lt":
                        case "Neq":
                            {
                                ParseCompareOperator(childNode, bitwiseOp);
                                break;
                            }
                        case "IsNotNull":
                            {
                                ParseIsNotNull(childNode, bitwiseOp);
                                break;
                            }
                        case "IsNull":
                            {
                                ParseIsNull(childNode, bitwiseOp);
                                break;
                            }
                        default:
                            break;
                    }
                }
            }
        }

        private void ParseOr(XmlNode orNode, BitwiseOp root)
        {
            OrOp orOp = new OrOp();
            ParseBitwiseOp(orNode, orOp, root);
        }

        private void ParseIsNull(XmlNode nullNode, BitwiseOp root)
        {
            XmlNode fieldRefNode = nullNode.SelectSingleNode("FieldRef");
            if (fieldRefNode == null)
            {
                throw new ParsingException("FieldRef node is required");
            }

            WhereTerm wt = WhereTerm.CreateIsNull(SqlExpression.Field(GetFieldName(fieldRefNode)));

            if (root == null)
            {
                whereTerms.Add(wt);
            }
            else
            {
                root.WhereTerms.Add(wt);
            }
        }

        private void ParseIsNotNull(XmlNode notNullNode, BitwiseOp root)
        {
            XmlNode fieldRefNode = notNullNode.SelectSingleNode("FieldRef");
            if (fieldRefNode == null)
            {
                throw new ParsingException("FieldRef node is required");
            }

            WhereTerm wt = WhereTerm.CreateIsNotNull(SqlExpression.Field(GetFieldName(fieldRefNode)));

            if (root == null)
            {
                whereTerms.Add(wt);
            }
            else
            {
                root.WhereTerms.Add(wt);
            }
        }

        private void ParseCompareOperator(XmlNode opNode, BitwiseOp root)
        {
            CompareOp co = new CompareOp(opNode.LocalName);
            string field = null;
            string value = null;

            ArgumentType secondArgType = ArgumentType.Constant;
            GetNodeFieldVaule(opNode, ref field, ref value, ref secondArgType);

            if (!string.IsNullOrEmpty(field) && !string.IsNullOrEmpty(value))
            {
                co.FirstArg = field;
                co.SecondArg = value;
                co.SecondArgType = secondArgType;
            }

            if (root == null)
            {
                compareOps.Add(co);
            }
            else
            {
                root.CompareOps.Add(co);
            }
        }

        private void GetNodeFieldVaule(XmlNode opNode, ref string fieldName, ref string value)
        {
            ArgumentType temp = ArgumentType.Constant;
            GetNodeFieldVaule(opNode, ref fieldName, ref value, ref temp);
        }

        private void GetNodeFieldVaule(XmlNode opNode, ref string fieldName, ref string value,
                                        ref ArgumentType argType)
        {
            XmlNode fieldRefNode = opNode.SelectSingleNode("FieldRef");
            if (fieldRefNode != null)
            {
                fieldName = GetFieldName(fieldRefNode);
            }
            else
            {
                throw new ParsingException("FieldRef node is required");
            }

            XmlNode valNode = opNode.SelectSingleNode("Value");
            if (valNode != null)
            {
                value = GetValue(valNode, ref fieldName, ref argType);
            }
            else
            {
                throw new ParsingException("Value node is required");
            }
        }

        private void ParseContains(XmlNode containsNode, BitwiseOp root)
        {
            string fieldName = null, value = null;
            GetNodeFieldVaule(containsNode, ref fieldName, ref value);
            WhereTerm wt = WhereTerm.CreateCompare(SqlExpression.Field(fieldName),
                                                   SqlExpression.Raw(string.Format("'%{0}%'", value)),
                                                   CompareOperator.Like);

            if (root == null)
            {
                whereTerms.Add(wt);
            }
            else
            {
                root.WhereTerms.Add(wt);
            }
        }

        private void ParseBeginsWith(XmlNode beginsWithNode, BitwiseOp root)
        {
            string fieldName = null, value = null;
            GetNodeFieldVaule(beginsWithNode, ref fieldName, ref value);
            WhereTerm wt = WhereTerm.CreateCompare(SqlExpression.Field(fieldName),
                                                   SqlExpression.Raw(string.Format("'{0}%'", value)),
                                                   CompareOperator.Like);

            if (root == null)
            {
                whereTerms.Add(wt);
            }
            else
            {
                root.WhereTerms.Add(wt);
            }
        }

        private void ParseOrderBy(XmlNode orderByNode)
        {
            foreach (XmlNode childNode in orderByNode.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                {
                    if (childNode.LocalName == "FieldRef")
                    {
                        string name = XmlConvert.DecodeName(childNode.Attributes["Name"].Value);

                        if (string.IsNullOrEmpty(name))
                            throw new ParsingException("\"Name\" attribute is required");

                        OrderByItem item = new OrderByItem();
                        item.FieldName = name;

                        XmlAttribute ascAttribute = childNode.Attributes["Ascending"];
                        if (ascAttribute != null)
                        {
                            string ascending = ascAttribute.Value;
                            bool asc;
                            if (bool.TryParse(ascending, out asc))
                            {
                                item.Ascending = asc;
                            }
                        }
                        orderBys.Add(item);
                    }
                }
            }
        }

        private string GetFieldName(XmlNode fieldRefNode)
        {
            if (fieldRefNode != null && fieldRefNode.NodeType == XmlNodeType.Element
                && fieldRefNode.LocalName == "FieldRef")
            {
                XmlAttribute nameAttrib = fieldRefNode.Attributes["Name"];
                if (nameAttrib == null || string.IsNullOrEmpty(nameAttrib.Value))
                    throw new ParsingException("\"Name\" attribute is required");

                string userField = XmlConvert.DecodeName(nameAttrib.Value);

                //MJM 2010 Doesn't have ContentType
                if (userField.Equals("ContentType", StringComparison.CurrentCultureIgnoreCase))
                {
                    if (!accessibleFields.ContainsKey(userField))
                        return "ContentType";
                }

                if (!allFields.ContainsKey(userField) && !accessibleFields.ContainsKey(userField))
                    throw new ParsingException("There is no field with name " + userField + " in list schema");

                string sqlField = accessibleFields[userField];

                return sqlField;
            }
            else
            {
                throw new ArgumentOutOfRangeException("fieldRefNode");
            }
        }

        private string GetValue(XmlNode valueNode, ref string field, ref ArgumentType argType)
        {
            if (valueNode != null && valueNode.NodeType == XmlNodeType.Element
                && valueNode.LocalName == "Value")
            {
                argType = ArgumentType.Constant;
                XmlNode userNode = valueNode.SelectSingleNode("UserID");
                if (userNode != null)
                {
                    argType = ArgumentType.UserId;
                    return userID.ToString(CultureInfo.InvariantCulture.NumberFormat);
                }

                XmlNode todayNode = valueNode.SelectSingleNode("Today");
                if (todayNode != null)
                {
                    DateTime res = DateTime.Today;
                    if (todayNode.Attributes.Count > 0)
                    {
                        string offsetType = todayNode.Attributes[0].Name;
                        string offsetValue = todayNode.Attributes[0].Value;

                        if (!string.IsNullOrEmpty(offsetValue) && !string.IsNullOrEmpty(offsetType))
                        {
                            int offset;
                            if (int.TryParse(offsetValue, out offset))
                            {
                                switch (offsetType)
                                {
                                    case "OffsetDays":
                                        {
                                            res = res.AddDays(offset);
                                            break;
                                        }
                                }
                            }
                        }
                    }

                    field = string.Format(onlyDateFunction, '[' + field + ']');
                    argType = ArgumentType.DateTime;
                    return string.Format(onlyDateFunction,
                                         '\'' + res.ToUniversalTime().ToString("yyyy-MM-dd") + '\'');
                }

                if (allFields.ContainsKey(valueNode.InnerText) ||
                    accessibleFields.ContainsKey(valueNode.InnerText))
                {
                    argType = ArgumentType.Field;
                }

                return valueNode.InnerText;
            }
            else
            {
                throw new ArgumentOutOfRangeException("valueNode");
            }
        }

        public OrderByTerm[] GetOrderByTerms()
        {
            List<OrderByTerm> res = new List<OrderByTerm>();
            foreach (OrderByItem orderByItem in orderBys)
            {
                if (accessibleFields.ContainsKey(orderByItem.FieldName))
                {
                    OrderByTerm obt = new OrderByTerm(accessibleFields[orderByItem.FieldName],
                        orderByItem.Ascending ? OrderByDirection.Ascending : OrderByDirection.Descending);
                    res.Add(obt);
                }
            }
            return res.ToArray();
        }

        public void WhereClauseAddTerms(WhereClause whereClause)
        {
            foreach (BitwiseOp bitwiseOp in bitwiseOps)
            {
                whereClause.SubClauses.Add(bitwiseOp.Render());
            }

            foreach (CompareOp compareOp in compareOps)
            {
                WhereClause wc;
                WhereTerm wt = compareOp.Render(out wc);
                if (wc != null)
                {
                    whereClause.SubClauses.Add(wc);
                }
                else
                {
                    whereClause.Terms.Add(wt);
                }
            }

            whereClause.Terms.AddRange(whereTerms.ToArray());
        }
    }
}